package es.ull.opendata.px2scovo.pcaxis.fileparser;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

//TODO: (PFC) Realizar documentación definitiva.
/*(Documentación provisional)
 * With this class we can parse a PC-Axis file.
 * 
 * @author Sergio Gonz&aacute;lez Cruz
 * @author Israel Beltr&aacute;n Valent&iacute;n
 * @version 0.1
 * @since JDK1.6
 *
 */

//TODO: (PFC) buscar la forma de dar soporte a los keywords, ya que estos son palabras reservadas que hay que controlar. Incluso tener en cuenta que algunas deben llevar variables y otras no, y también que unas son campos obligatorios y otros no.
public class PXParser {

	private File fileIn;

	private PXKey parseKey(String key) throws PXParserException {
		// TODO: (PFC) Realizar documentación definitiva.
		/*
		 * (documentación provisional) Expresiones regulares:
		 * 
		 * - variable: son cadenas de caracteres entre paréntesis y con comillas.
		 * La cadena vacía no se considera una variable.
		 * 
		 * - onlyKeyword: es cualquier cadena que no contenga paréntesis.
		 */

		String simpleKeyPattern = "^[^()]+$"; // SOURCE=Instituto Klingon de
															// Estadística
		String variable = "\"([^\"]+)\"";
		String keyPattern = "^(.+)\\(" + variable + "(," + variable + ")*\\)$"; // VALUE("Planeta de Residencia")="Tierra","Vulcano","Marte"
																										// DATANOTECELL("var1",
																										// "var2",
																										// "var3")
																										// =
																										// "value";
		// WARNING: Multivariable keywords are not supported. This method will
		// return only the first variable.
		if (Pattern.matches(simpleKeyPattern, key)) {
			return new PXKey(key);
		} else if (Pattern.matches(keyPattern, key)) {
			try {
				Matcher matcher = Pattern.compile(keyPattern).matcher(key);
				matcher.find();
				return new PXKey(matcher.group(1), matcher.group(2));
			} catch (IllegalStateException e) {
				throw new PXParserException("Invalid key found " + key, e);
			}
		} else {
			throw new PXParserException("Unrecognized key found " + key);
		}
	}

	private Object parseValue(String key, String value) throws PXParserException {
		// TODO: (PFC) Realizar documentación definitiva.
		/*
		 * (documentación provisional) Expresiones regulares:
		 * 
		 * - Se considera un separador un espacio o una coma.
		 * 
		 * - Se considera una lista de literales a al menos dos literales
		 * separados por uno de los separadores. Y si la lista posee más de dos
		 * literales el separador debe ser el mismo.
		 */

		String unquotedLiteral = "[^, ]+"; // Literals without quote marks.
		String quotedLiteral = "\"[^\"]+\"";// Literals with quote marks.
		String literal = "(" + quotedLiteral + "|" + unquotedLiteral + ")";

		String onlyOneLiteral = "^" + literal + "$"; // There are only one
																	// literal.

		if (Pattern.matches(onlyOneLiteral, value)) {
			if (key.equals("VALUES") || key.equals("CODES") || key.equals("DATA")) {
				ArrayList<String> simple = new ArrayList<String>();
				simple.add(value.replaceAll("\"", ""));
				return simple;
			} else {
				return value.replaceAll("\"", "");			
			}
		} else {
			ArrayList<String> literals = new ArrayList<String>();
			Matcher getLiteals = Pattern.compile(literal).matcher(value);
			while (getLiteals.find()) {
				literals.add(getLiteals.group().replaceAll("\"", ""));
			}
			return literals;
		}
	}

	public static String PXENCODING = "Cp1252";

	public PXParser(File fileIn) {
		super();
		this.fileIn = fileIn;
	}

	@SuppressWarnings("unchecked")
	public Map<String, Object> parseFile() throws PXParserException {
		PXReader pxReader;
		Map<String, Object> pxPropertyMap = new HashMap<String, Object>();

		// REGEXP BUILDING
		// Key token:
		String key = "^([^=]+)";
		// Regexp that represents the types of values.
		String value = "(.*)$";

		// Types of sentences.
		String sentence = key + "=" + value;

		Pattern pxPattern = Pattern.compile(sentence);

		// PARSING
		try {
			FileInputStream fis = new FileInputStream(fileIn);
			InputStreamReader isr = new InputStreamReader(fis, PXENCODING);
			pxReader = new PXReader(isr);

			String line;

			// This if sentence avoids empty files.
			if ((line = pxReader.readLine()) == null) {
				throw new PXParserException("This file is Empty File");
			}

			do {
				Matcher pxMatcher = pxPattern.matcher(line);
				if (!pxMatcher.matches()) {
					throw new PXParserException("Invalid line found: <" + line + ">.");
				}

				PXKey mapKey = parseKey(pxMatcher.group(1));
				Object mapValue = parseValue(mapKey.getKeyword(), pxMatcher.group(2));

				if (mapKey.hasVariable()) {
					try {
						LinkedHashMap<String, Object> categoriesPerVariable = (LinkedHashMap<String, Object>) pxPropertyMap.get(mapKey.getKeyword());
						if (categoriesPerVariable == null) {
							categoriesPerVariable = new LinkedHashMap<String, Object>();
							pxPropertyMap.put(mapKey.getKeyword(), categoriesPerVariable);
						}
						categoriesPerVariable.put(mapKey.getVariable(), mapValue);
					} catch (ClassCastException e) {
						pxPropertyMap.put(mapKey.getKeyword(), mapKey.getVariable());
					}
				} else {
					pxPropertyMap.put(mapKey.getKeyword(), mapValue);
				}
			} while ((line = pxReader.readLine()) != null);

		} catch (FileNotFoundException e) {
			throw new PXParserException("Error. File not found.", e);
		} catch (IOException e) {
			throw new PXParserException("Error when reading File.", e);
		}

		return pxPropertyMap;
	}
}

class PXKey {
	private String keyword;
	private String variable;

	public PXKey(String keyword) {
		super();
		this.keyword = keyword;
		this.variable = null;
	}

	public PXKey(String keyword, String variable) {
		super();
		this.keyword = keyword;
		this.variable = variable;
	}

	public String getKeyword() {
		return keyword;
	}

	public String getVariable() {
		return variable;
	}

	public boolean hasVariable() {
		return variable != null;
	}
}